﻿using System;
using System.Collections.Generic;
using System.Drawing;
using Petra.Gis.Geometry.Support;

namespace Petra.Gis.RenderingServices.PlanarBase 
{


  public class MapExtent
  {


    public enum EdgeName
    {
      left = 2, bottom = 8, right = 1, top = 4
    }

    protected MapBase _parentMap;


    protected double[] _Bbox = new double[9];
    protected int[] _ScrBbox = new int[9];

    //public MapExtent(PlanarMap parentMap, BoundingBox crsBounds, DisplayTransformMatrix transform)
    //  : this(parentMap, crsBounds.Left, crsBounds.Bottom, crsBounds.Right, crsBounds.Top, transform)
    public MapExtent(MapBase parentMap, BoundingBox crsBounds)
      : this(parentMap, crsBounds.Left, crsBounds.Bottom, crsBounds.Right, crsBounds.Top)
    {
    }


    //public MapExtent(PlanarMap parentMap, double worldLeft, double worldBottom, double worldRight, double worldTop, DisplayTransformMatrix transform)
    public MapExtent(MapBase parentMap, double worldLeft, double worldBottom, double worldRight, double worldTop)
    {

      _parentMap = parentMap;

      //validate
      if (!isValid(worldLeft, worldBottom, worldRight, worldTop))
        throw new ArgumentException("invalid bbox values", "left, right, top, bottom");

      //set values
      _Bbox[2] = worldLeft;
      _Bbox[8] = worldBottom;
      _Bbox[1] = worldRight;
      _Bbox[4] = worldTop;

      _transMatrix = new DisplayTransformMatrix();
      //resize
      Resize(parentMap.Width, parentMap.Height, 0);

      //_transMatrix = transform;

      Point tmp = _transMatrix.TransformPoint(worldLeft, worldBottom);
      _ScrBbox[2] = tmp.X;
      _ScrBbox[8] = tmp.Y;

      tmp = _transMatrix.TransformPoint(worldRight, worldTop);

      _ScrBbox[1] = tmp.X;
      _ScrBbox[4] = tmp.Y;
      
    }




    public double Left
    {
      get { return _Bbox[2]; }
    }




    public double Bottom
    {
      get { return _Bbox[8]; }
    }




    public double Right
    {
      get { return _Bbox[1]; }
    }




    public double Top
    {
      get { return _Bbox[4]; }
    }




    public double Width
    {
      get { return Math.Abs(this.Right - this.Left); }
    }




    public double Height
    {
      get { return Math.Abs(this.Top - this.Bottom); }
    }


    //public double this[EdgeName edgeName]
    //{
    //  get { return _Bbox[(int)edgeName]; }
    //}


    protected DisplayTransformMatrix _transMatrix;

    public DisplayTransformMatrix DisplayTransform
    {
      get {return _transMatrix;}
      set {_transMatrix = value;}
    }



    protected bool isValid(double left, double bottom, double right, double top)
    {

      if (left < right && bottom < top)
        return true;
      else
        return false;
    }




    //int z = Convert.ToInt32("11001001", 2);
    /* for line:
     * for start point uses xxxx0000, first 4 bits
     * for end point   uses 0000xxxx, last  4 bits
     * for point only  uses xxxx0000, first 4 bits
     * 
     *         |        |
     *   0110  |  0100  |  0101
     *         |        |   
     * --------------------------
     *         |        |
     *   0010  |  0000  |  0001
     *         |        |
     * --------------------------
     *         |        |   
     *   1010  |  1000  |  1001
     *         |        |   
     */
    /*
    internal int getLineIntesectMask(Point startP, Point endP)
    {
      int xs, xe;
      int ys, ye;

      xs = (startP.X <= _Bbox.left ? 2 : 0);
      xs = (startP.X <= _Bbox.right && xs == 2 ? 2 : 1);
      ys = (startP.Y <= _Bbox.bottom ? 8 : 0);
      ys = (startP.Y <= _Bbox.top && ys == 8 ? 8 : 4);

      xe = (endP.X <= _Bbox.left ? 32 : 0);
      xe = (endP.X <= _Bbox.right && xe == 32 ? 32 : 16);
      ye = (endP.Y <= _Bbox.bottom ? 128 : 0);
      ye = (endP.Y <= _Bbox.top && ye == 128 ? 128 : 64);
      return xs | ys | xe | ye;
    }
    
    */
    protected internal int getPointIntesectMask(Point pnt)
    {
      int xs;
      int ys;

      xs = (pnt.X <= _Bbox[(int)EdgeName.left] ? 2 : 0);
      xs = (pnt.X <= _Bbox[(int)EdgeName.right] && xs == 2 ? 2 : 1);
      ys = (pnt.Y <= _Bbox[(int)EdgeName.bottom] ? 8 : 0);
      ys = (pnt.Y <= _Bbox[(int)EdgeName.top] && ys == 8 ? 8 : 4);

      return xs | ys;
    }




    private Point[] noIntersection = new Point[2]; //avoid reinitialization with each call

    public Point[] lineIntersect(PointD startPoint, PointD endPoint)
    {
      
      Point startPt, endPt;

      startPt = _transMatrix.TransformPoint(startPoint);
      endPt = _transMatrix.TransformPoint(endPoint);

      return lineIntersect(startPt, endPt);
    }




    public Point[] lineIntersect(Point startPoint, Point endPoint)
    {

      Point tmpPt;

      //get point sone intersection mask
      int ims = getPointIntesectMask(startPoint);
      int ime = getPointIntesectMask(endPoint);

      if ((ims | ime) == 0) //both inside of MapRegion
      {
        noIntersection[0] = startPoint;
        noIntersection[1] = endPoint;
        return noIntersection;
      }

      //one inside one outside, get intersection point and draw
      else if ((ims | ime) != 0 && (ims == 0 || ime == 0))
      {
        if (ims == 0) //start point inside bbox
          tmpPt = getIntersectionPoint(startPoint, endPoint, ime);
        else          //end point inside bbox
          tmpPt = getIntersectionPoint(startPoint, endPoint, ims);

        noIntersection[0] = startPoint;
        noIntersection[1] = tmpPt;
        return noIntersection;
      }

      //none inside, different zones, can have 1-4 intersection points !!
      else if ((ims | ime) != 0 && ims != 0 && ime != 0)
      {
        //XOR (^) gives full list of zones to search intersection for
        List<Point> tmpPts = getOrderedIntersectionPoints(startPoint, endPoint, (ims ^ ime));
        return tmpPts.ToArray();
      }
      else
        return null; //no intersection
    }





    public Point[] lineIntersect(PointF startPt, PointF endPt)
    {
      PointD startPoint = new PointD();
      PointD endPoint = new PointD();

      startPoint.X = startPt.X;
      startPoint.Y = startPt.Y;

      endPoint.X = endPt.X;
      endPoint.Y = endPt.Y; 
      
      return lineIntersect(startPoint, endPoint);
    }





    protected internal List<Point> getOrderedIntersectionPoints(Point startP, Point endP, int im)
    {
      List<Point> tmpPts = new List<Point>();

      if ((im & (int)(int)EdgeName.right) != 0) //resolve right edge
        tmpPts.Add(getEdgeIntersectionPoint(startP, endP, EdgeName.right, true));

      if ((im & (int)(int)EdgeName.top) != 0) //resolve top edge
        tmpPts.Add(getEdgeIntersectionPoint(startP, endP, EdgeName.top, true));

      if ((im & (int)(int)EdgeName.left) != 0) //resolve left edge
        tmpPts.Add(getEdgeIntersectionPoint(startP, endP, EdgeName.left, true));

      if ((im & (int)(int)EdgeName.bottom) != 0) //resolve bottom edge
        tmpPts.Add(getEdgeIntersectionPoint(startP, endP, EdgeName.bottom, true));

      //order all point by x or y distance from start point
      tmpPts.Sort(new PointDistanceComparer(startP));
      return tmpPts;
    }




    protected internal Point getIntersectionPoint(Point startP, Point endP, int im)
    {
      Point tmpPt;

      if ((im & (int)(int)EdgeName.right) != 0) //resolve right edge
      {
        tmpPt = getEdgeIntersectionPoint(startP, endP, EdgeName.right, false);
        if (this.Bottom <= tmpPt.Y || tmpPt.Y <= this.Top)
          return tmpPt;
      }

      if ((im & (int)(int)EdgeName.top) != 0) //resolve top edge
      {
        tmpPt = getEdgeIntersectionPoint(startP, endP, EdgeName.top, false);
        if (this.Left <= tmpPt.X || tmpPt.X <= this.Right)
          return tmpPt;
      }

      if ((im & (int)EdgeName.left) != 0) //resolve left edge
      {
        tmpPt = getEdgeIntersectionPoint(startP, endP, EdgeName.left, false);
        if (this.Bottom <= tmpPt.Y || tmpPt.Y <= this.Top)
          return tmpPt;
      }

      if ((im & (int)EdgeName.bottom) != 0) //resolve bottom edge
      {
        tmpPt = getEdgeIntersectionPoint(startP, endP, EdgeName.bottom, false);
        if (this.Left <= tmpPt.X || tmpPt.X <= this.Right)
          return tmpPt;
      }

      throw new InvalidOperationException(); //never reach here !!

    }




    protected internal Point getEdgeIntersectionPoint(Point startP, Point endP, EdgeName intersectEdge, bool fitByEdge)
    {

      Point tmpPt = new Point();
      int edgeId = (int)intersectEdge;

      if (intersectEdge == EdgeName.left || intersectEdge == EdgeName.right)
      {
        tmpPt.Y = startP.Y + ((endP.Y - startP.Y) * (_ScrBbox[edgeId] - startP.X) / (endP.X - startP.X));
        tmpPt.X = _ScrBbox[edgeId];
      }
      else
      {
        tmpPt.X = startP.X + ((endP.X - startP.X) * (_ScrBbox[edgeId] - startP.Y) / (endP.Y - startP.Y));
        tmpPt.Y = _ScrBbox[edgeId];
      }

      if (fitByEdge)
      {
        if (tmpPt.Y < _ScrBbox[(int)EdgeName.bottom])
          tmpPt.Y = _ScrBbox[(int)EdgeName.bottom];

        if (tmpPt.Y > _ScrBbox[(int)EdgeName.top])
          tmpPt.Y = _ScrBbox[(int)EdgeName.top];

        if (tmpPt.X < _ScrBbox[(int)EdgeName.left])
          tmpPt.X = _ScrBbox[(int)EdgeName.left];

        if (tmpPt.X > _ScrBbox[(int)EdgeName.right])
          tmpPt.X = _ScrBbox[(int)EdgeName.right];
      }

      return tmpPt;

    }


    /// <summary>
    /// updates map world coordinate extent by given width and height 
    /// of map display region (viewport) along current map center
    /// </summary>
    /// <param name="Width">viewport width</param>
    /// <param name="Height">viewport htight</param>
    public void Resize(float width, float height, double mapScale)
    {

      double WrldWidth = this.Right - this.Left;
      double WrldHeight = this.Top - this.Bottom;
      double ScaleX = Math.Abs(WrldWidth / width);
      double ScaleY = Math.Abs(WrldHeight / height);

      double tmpScale;

      if(mapScale == 0)
        tmpScale = ScaleX;
      else
        tmpScale = mapScale;
      
      double OldWrldWidth = WrldWidth;
      double OldWrldHeight = WrldHeight;

      WrldWidth = width * tmpScale;
      WrldHeight = height * tmpScale;


      //_Bbox[2] = worldLeft;
      //_Bbox[8] = worldBottom;
      //_Bbox[1] = worldRight;
      //_Bbox[4] = worldTop;


      _Bbox[2] = (_Bbox[2] + ((OldWrldWidth - WrldWidth) / 2));
      _Bbox[8] = (_Bbox[8] - ((OldWrldHeight - WrldHeight) / 2));
      _Bbox[1] = (_Bbox[2] + WrldWidth);
      _Bbox[4] = (_Bbox[8] - WrldHeight);

      //update display transform matrix
      _transMatrix.Translate(_Bbox[2], _Bbox[8]);


      //todo: if map is not in pixels update scale to precision !!
      if (_parentMap.DisplayPaperUnits != MapPaperUnits.px)
        throw new ArgumentException("not implemented sorry");

      double scale = (this.Right - this.Left) / Width;
      _transMatrix.Scale(scale, (-1) * scale);

    }




    public BoundingBox Bounds
    {
      get { return new BoundingBox(this.Left, this.Bottom, this.Right, this.Top); }
    }

  }
}
